TIL.40) [DRF] Tutorial Part5. Relationships & Hyperlinked APIs

해당 내용은 Django REST Framework의 공식문서를 참고하여 작성되었으며. 해당 공식문서는 여기에서 확인 가능하다.

현재까지 작성한 API의 관계는 Primary Key를 사용하여 표시하였다. 이제 Django REST Framework에서 제공하는 API Hyperlink기능을 사용하여 관계를 표현하고 API의 응집력과 검색가능성을 개선하겠다.

Creating an endpoint for the root of our API

지금까지는 Snippet과 User에 대한 Endpoint는 있었지만 Root 진입에 대한 Endpoint는 없었다. 먼저 Root 진입에 대한 Endpoint를 snippets/views.py에 FBV 패턴으로 추가할 것이며 이를 위해 @api_viewDecorator를 사용할 것이다.

# snippets/views.py

from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.reverse import reverse

@api_view(['GET'])
def api_root(request, format=None):
    return Response({
        'users': reverse('user-list', request=request, format=format),
        'snippets': reverse('snippet-list', request=request, format=format)
    })

전체적인 snippets/views.py는 다음과 같다.

# snippets/views.py

from django.contrib.auth.models import User
from rest_framework import (
    generics,
    permissions,
    renderers
)
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.reverse import reverse
from .models import Snippet
from .serializers import (
    SnippetSerializer,
    UserSerializer
)
from .permissions import IsOwnerOrReadOnly

@api_view(['GET'])
def api_root(request, format=None):
    return Response({
        'users' : reverse('user-list', request=request, format=format),
        'snippets' : reverse('snippet-list', request=request, format=format)
    })

class SnippetList(generics.ListCreateAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]
    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]

class UserList(generics.ListAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

class UserDetail(generics.RetrieveAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

여기서 확인해야 할 사항이 두가지 있다.

  1. 정규화된 URL을 반환하기 위해 Django REST Framework에서 제공하는 reverse기능을 사용한다.
  2. URL패턴은 나중에 snippets/urls.py에서 선언할 name으로 식별한다.

Creating an endpoint for the highlighted snippets

지금까지 작성한 API에서 아직 구현이 안된 부분은 Code highlight기능이다.

다른 API Endpoint와 달리 JSON을 사용하지 않고 HTML표현을 사용한다. Django REST Framework에서 제공되는 HTML Renderer에는 두가지 스타일이 있다.

  1. 템플렛을 사용하여 렌더링된 HTML을 처리
  2. 미리 렌더링된 HTML을 처리

여기는 두번째 스타일을 사용 할 것이다.

Code Highlight View를 생성 할 때 고려해야할 또 다른 사항은 사용할 수 있는 기존의 구체적인 Generic View가 없다는 것이다. 왜냐하면 객체 인스턴스를 반환하지 않고 객체 인스턴스의 속성을 반환하기 때문이다.

snippets/views.py에 구체적인 Generic View를 사용하는 대신 인스턴스를 나타내는 CBV패턴을 사용하고 자체적인 GET동작을 구현하기 위해 .get()메서드를 생성한다.

# snippets/views.py

from rest_framework import renderers
from rest_framework.response import Response

class SnippetHighlight(generics.GenericAPIView):
    queryset = Snippet.objects.all()
    renderer_classes = [renderers.StaticHTMLRenderer]

    def get(self, request, *args, **kwargs):
        snippet = self.get_object()
        return Response(snippet.highlighted)

전체적인 snippets/views.py의 모습은 다음과 같다

# snippets/views.py

from django.contrib.auth.models import User
from rest_framework import (
    generics,
    permissions,
    renderers
)
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.reverse import reverse
from .models import Snippet
from .serializers import (
    SnippetSerializer,
    UserSerializer
)
from .permissions import IsOwnerOrReadOnly

@api_view(['GET'])
def api_root(request, format=None):
    return Response({
        'users' : reverse('user-list', request=request, format=format),
        'snippets' : reverse('snippet-list', request=request, format=format)
    })

class SnippetList(generics.ListCreateAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]
    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]

class UserList(generics.ListAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

class UserDetail(generics.RetrieveAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

class SnippetHighlight(generics.GenericAPIView):
    queryset = Snippet.objects.all()
    renderer_classes = [renderers.StaticHTMLRenderer]

    def get(self, request, *args, **kwargs):
        snippet = self.get_object()
        return Response(snippet.highlighted)

이제 API Root에 대한 URL과 Code Highlight에 대한 URL을 snippets/urls.py에 추가해준다.

# snippets/urls.py

...
urlpatterns = [
    path('', api_root),
    path('/snippets', SnippetList.as_view()),
    path('/snippets/<int:pk>', SnippetDetail.as_view()),
    path('/snippets/<int:pk>/highlight', SnippetHighlight.as_view()),
    path('/snippets/users', UserList.as_view()),
    path('/snippets/users/<int:pk>', UserDetail.as_view()),
]
...

Hyperlinking our API

Entity 간의 관계를 다루는 것은 WEB API 디자인에서 가장 어려운 측면 중 하나이다. 관계를 표현하기 위해 선택 할 수 있는 여러 옵션은 다음과 같다.

  • Primary Key사용
  • Entity 간 하이퍼링크 사용
  • 관련 Entity에서 고유한 식별 슬러그 필드 사용
  • 관려 Entity의 기본 문자열 표현을 사용
  • 상위 표현 내 관련 Entity 중첩
  • 다른 사용자 지정 표현

Django REST Framework는 이러한 모든 옵션을 지원하고 정참조 또는 역참조 관계에 적용하거나 Generic foreign key와 같은 사용자 지정 관리자에 적용 할 수 있다.

여기서는 Entity간에 Hyperlinked style을 사용 할 것이다. 이를 위해 snippets/serializers.py에 기존 ModelSerializerHyperlinkedModelSerializer로 변경하면서 이와 관련된 수정도 함께 진행 할 것이다.

⚠️ HyperlinkedModelSerializerModelSerializer의 차이점

  • 기본적으로 id필드는 포함되지 않는다.
  • HyperlinkedIdentityField를 사용하는 url필드를 포함한다.
  • 관계는 PrimaryKeyRelatedField대신 HyperlinkedRelatedField를 사용한다.
# snippets/serializers.py

from django.contrib.auth.models import User
from rest_framework import serializers
from .models import (
    Snippet,
    LANGUAGE_CHOICES,
    STYLE_CHOICES
)

class SnippetSerializer(serializers.HyperlinkedModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.username')
    highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html')

    class Meta:
        model = Snippet
        fields = ['url', 'id', 'highlight', 'title',
                  'code', 'linenos', 'language', 'style', 'owner']

class UserSerializer(serializers.HyperlinkedModelSerializer):
    snippets = serializers.HyperlinkedRelatedField(many=True, view_name='snippet-detail', read_only = True)

    class Meta:
        model = User
        fields = ['url', 'id', 'username', 'snippets']

highlight필드도 추가되었다. 이 필드는 snippet-detail이라는 url패턴 대신 snippet-highlight라는 url패턴을 가리키는 점을 제외하고 기본적으로 url field와 동일한 유형이다.

snippets/urls.py에서 format_suffix_patterns를 통해 .json과 같은 타입을 포함했기 때문에 highlight가 반환하는 타입을 명시적으로 지정해야 한다 따라서 format의 인수로 .html을 사용하겠다고 명시해야할 필요가 있다.

Making sure our URL patterns are named

하이퍼링크 된 API를 사용하려면 urlpatterns에 URL이름을 지정해야 한다. 먼저 지정해야할 URL패턴들을 살펴보면 다음과 같다.

  • Root API는 user-listsnippet-list를 참조한다.
  • SnippetSerializer에는 snippet-highlight를 참조하는 highlight필드가 있다.
  • UserSerializer에는 snippet-detail을 참조하는 snippets필드가 있다.
  • SnippetSerializerUserSerializer에는 {model_name}-detail을 참조하는 url필드가 포함되어 있으며 이 경우 snippet-detailuser-detail이 해당된다.

이제 snippets/urls.py에 위와 같은 사항을 참조하여 name을 부여하자.

# snippets/urls.py

from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from .views import (
    SnippetList,
    SnippetDetail,
    UserList,
    UserDetail,
    SnippetHighlight,
    api_root
)

urlpatterns = [
    path('', api_root),
    path('/snippets', SnippetList.as_view(), name='snippet-list'),
    path('/snippets/<int:pk>', SnippetDetail.as_view(), name='snippet-detail'),
    path('/snippets/<int:pk>/highlight', SnippetHighlight.as_view(), name='snippet-highlight'),
    path('/snippets/users', UserList.as_view(), name='user-list'),
    path('/snippets/users/<int:pk>', UserDetail.as_view(), name='user-detail'),
]

urlpatterns = format_suffix_patterns(urlpatterns)

Adding pagination

User 및 Code snippet에 대한 List는 많은 인스턴스를 반환 할 수 있으므로 결과에 페이지 번호를 매기고 API 클라이언트가 각 ㅂ개별 페이지를 단계별로 접근하도록 만들 수 있다.

이를 위해 프로젝트 단의 settings.py에 Django REST Framework가 제공하는 pagination을 추가 할 수 있다. 여기서는 tutorial/settings.py에 추가하도록 하겠다.

# tutorial/settings.py

...
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10
}

Django REST Framework의 설정은 모두 REST_FRAMEWORK네임 스페이스에 추가하여 다른 프로젝트 설정과 구분하도록 만들어야 한다.

필요한 경우 pagination은 사용자 지정설정으로 추가할 수 있지만 여기서는 일단 기본값을 그대로 사용하겠다.

Browsing the API

이제 브라우저를 열고 Code Snippets의 Detail로 이동하면 해당 Code Snippet의 Highlight링크가 생성되었으며 이를 클릭하면 Code Highlight된 HTML페이지로 이동된다.

https://yongineer.duckdns.org/django/til40_1.png

결과



Written by@Yongineer
Backend Developer

GitHubInstagram